use std::path::Path;

use indoc::indoc;
use insta::assert_snapshot;
use move_package_alt::test_utils::{basic_manifest, graph_builder::TestPackageGraph};
use test_log::test;

#[allow(unused)]
use tracing::debug;

/// Ensure that the directory containing mock-resolver is on the PATH
fn add_bindir() {
    let bindir = Path::new(std::env!("CARGO_BIN_EXE_mock-resolver"))
        .parent()
        .unwrap()
        .to_string_lossy();

    // TODO: replace this with different logic
    // SAFETY: this is safe because it's run under cargo nextest run. See:
    // `https://nexte.st/docs/configuration/env-vars/`
    unsafe {
        std::env::set_var(
            "PATH",
            format!("{}:{}", std::env::var("PATH").unwrap(), bindir),
        );
    }
}

/// Basic external resolver test
#[test(tokio::test)]
async fn external_basic() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies.b.r.mock-resolver]
        stderr = "foo"
        output._test_env_id.result = { local = "../b" }
        "###
    ));

    let scenario = TestPackageGraph::new(["root", "b"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    let mut root_pkg = scenario.root_package("root").await;
    root_pkg.save_lockfile_to_disk().unwrap();
    let lockfile = scenario.read_file("root/Move.lock");
    assert_snapshot!(lockfile, @r###"
    # Generated by move; do not edit
    # This file should be checked in.

    [move]
    version = 4

    [pinned._test_env.a]
    source = { local = "../a" }
    use_environment = "_test_env"
    manifest_digest = "8837576FAADC06A89D5561E208B0CEF18F6EE6728C387038439D1DD8C796223E"
    deps = { b = "b" }

    [pinned._test_env.b]
    source = { local = "../b" }
    use_environment = "_test_env"
    manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363"
    deps = {}

    [pinned._test_env.root]
    source = { root = true }
    use_environment = "_test_env"
    manifest_digest = "C8B4192A01C7A591BC22462B08A4FDF822C115A5C6B7BD0B46957367D963DB70"
    deps = { a = "a" }
    "###);
}

/// Two different external resolvers are used
#[test(tokio::test)]
async fn external_multiple_resolvers() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies.res_1.r.mock-resolver]
        output._test_env_id.result = { local = "../res_1" }

        [dependencies.res_2.r.mock-resolver-2]
        output._test_env_id.result = { local = "../res_2" }
        "###
    ));

    let scenario = TestPackageGraph::new(["root", "res_1", "res_2"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    let mut root_pkg = scenario.root_package("root").await;
    root_pkg.save_lockfile_to_disk().unwrap();
    let lockfile = scenario.read_file("root/Move.lock");
    assert_snapshot!(lockfile, @r###"
    # Generated by move; do not edit
    # This file should be checked in.

    [move]
    version = 4

    [pinned._test_env.a]
    source = { local = "../a" }
    use_environment = "_test_env"
    manifest_digest = "FF85BD52952A1C8221999FD84F20BA90F2B03DD9CA6B5EEC6A212E7735F531AF"
    deps = { res_1 = "res_1", res_2 = "res_2" }

    [pinned._test_env.res_1]
    source = { local = "../res_1" }
    use_environment = "_test_env"
    manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363"
    deps = {}

    [pinned._test_env.res_2]
    source = { local = "../res_2" }
    use_environment = "_test_env"
    manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363"
    deps = {}

    [pinned._test_env.root]
    source = { root = true }
    use_environment = "_test_env"
    manifest_digest = "C8B4192A01C7A591BC22462B08A4FDF822C115A5C6B7BD0B46957367D963DB70"
    deps = { a = "a" }
    "###);
}

/// External resolver returns a dependency that isn't correctly formatted
#[test(tokio::test)]
async fn external_bad_schema() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies.b.r.mock-resolver]
        output._test_env_id = { result = { not-a-dep = "" } }
        stderr = "stderr output"
        "###
    ));

    let scenario = TestPackageGraph::new(["root"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    assert_snapshot!(scenario.root_package_err("root").await,
        @"Error while loading dependency <ROOT>/a: `mock-resolver` did not follow the external resolver protocol (response was not serialized correctly)"
    );
}

/// Broken external resolver returns no output
#[test(tokio::test)]
async fn external_empty_output() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies.mock.r.mock-resolver]
        stdout = ""
        stderr = "bad output"
        "###
    ));

    let scenario = TestPackageGraph::new(["root"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    assert_snapshot!(scenario.root_package_err("root").await,
        @"Error while loading dependency <ROOT>/a: `mock-resolver` did not follow the external resolver protocol (response was not serialized correctly)"
    );
}

/// Broken external resolver returns correct output but exits with an error code
#[test(tokio::test)]
async fn external_errorcode() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies.mock.r.mock-resolver]
        exit_code = 1
        stdout = """\
        [ { "jsonrpc": "2.0", "id": 0, "result": { "local": "for_default" } } ]
        """
        stderr = "stderr output"
        "###
    ));

    let scenario = TestPackageGraph::new(["root"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    assert_snapshot!(scenario.root_package_err("root").await,
        @"Error while loading dependency <ROOT>/a: `mock-resolver` returned error code: exit status: 1"
    );
}

/// External resolver receives two requests but only returns one response
#[test(tokio::test)]
async fn external_missing_keys() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies.mock1.r.mock-resolver]
        stdout = """\
        [ { "jsonrpc": "2.0", "id": 0, "result": { "local": "for_default" } } ]
        """

        [dependencies.mock2.r.mock-resolver]
        stdout = """\
        [ { "jsonrpc": "2.0", "id": 0, "result": { "local": "for_default" } } ]
        """
        stderr = "stderr output"
        "###
    ));

    let scenario = TestPackageGraph::new(["root"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    assert_snapshot!(scenario.root_package_err("root").await,
        @"Error while loading dependency <ROOT>/a: `mock-resolver` did not follow the external resolver protocol (received wrong set of responses)"
    );
}

/// external resolver produces non-json output
#[test(tokio::test)]
async fn external_non_json() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies.mock.r.mock-resolver]
        stdout = "this isn't json"
        stderr = "stderr output"
        "###
    ));

    let scenario = TestPackageGraph::new(["root"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    assert_snapshot!(scenario.root_package_err("root").await,
        @"Error while loading dependency <ROOT>/a: `mock-resolver` did not follow the external resolver protocol (response was not serialized correctly)"
    );
}

/// Broken external resolver produces another external dependency
#[test(tokio::test)]
async fn external_returns_external() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies.mock.r.mock-resolver]
        output._test_env_id.result = { r.mock-resolver = "." }
        stderr = "stderr output"
        "###
    ));

    let scenario = TestPackageGraph::new(["root"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    assert_snapshot!(scenario.root_package_err("root").await,
        @"Error while loading dependency <ROOT>/a: `mock-resolver` did not follow the external resolver protocol (response was not serialized correctly)"
    );
}

/// External resolver is not found
#[test(tokio::test)]
async fn external_no_resolver() {
    add_bindir();

    let mut a_manifest = basic_manifest("a", "v0.0.0");
    a_manifest.push_str(indoc!(
        r###"
        [dependencies]
        foo = { r.abcdef = "nope" }
        "###
    ));

    let scenario = TestPackageGraph::new(["root"])
        .add_package("a", |a| a.add_file("Move.toml", a_manifest))
        .add_deps([("root", "a")])
        .build();

    assert_snapshot!(scenario.root_package_err("root").await,
        @"Error while loading dependency <ROOT>/a: External resolver `abcdef` not found; ensure that it is installed and on your PATH"
    );
}
